package bancosys.tec.persist.dataset;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import jmine.tec.persist.PersistMessages;

import org.apache.commons.beanutils.PropertyUtils;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;

import bancosys.tec.persist.exception.PersistenceException;

/**
 * Classe utilizada para paginar os resultados do hibernate na geração de relatórios, evitando assim que todos os resultados estejam na
 * memória ao mesmo tempo.
 * 
 * @created Apr 25, 2007
 * @author Gustavo Almeida
 * @param <T> tipo devolvido pelo ReportDataSource
 */
public class HibernateRandomAccessResultSet<T> implements RandomAccessResultSet<T>, Iterable<T> {
    private Criteria criteria;

    private Query query;

    private List<T> resultPage;

    private int pageStart = Integer.MAX_VALUE;

    private int pageEnd = Integer.MIN_VALUE;

    private final int jdbcFetchSize;

    private final int pageSize;

    private static final int DEFAULT_PAGE_SIZE = 500;

    private static final int DEFAULT_FETCH_SIZE = 100;

    private String[] fieldLazy;

    /**
     * Construtor com suporte a receber um array de nomes de atributos lazy para carregar
     * 
     * @param session Sessao do Hibernate
     * @param criteria Criterio da busca
     * @param fieldLazy Lista de Atributos lazy para carga
     */
    public HibernateRandomAccessResultSet(Session session, Criteria criteria, String[] fieldLazy) {
        this(session, criteria);

        this.fieldLazy = fieldLazy;
    }

    /**
     * Construtor com suporte a receber um array de nomes de atributos lazy para carregar
     * 
     * @param session Sessao do Hibernate
     * @param query Query da busca
     * @param fieldLazy Lista de Atributos lazy para carga
     */
    public HibernateRandomAccessResultSet(Session session, Query query, String[] fieldLazy) {
        this(session, query);

        this.fieldLazy = fieldLazy;
    }

    /**
     * Construtor.
     * 
     * @param session session do hibernate.
     * @param criteria criteria
     * @param jdbcFetchSize <code>Fetch Size</code> do <code>JDBC</code>.
     * @param pageSize tamanho da página da query.
     * @see Criteria#setFetchSize(int)
     */
    public HibernateRandomAccessResultSet(Session session, Criteria criteria, int jdbcFetchSize, int pageSize) {
        this.criteria = criteria;
        this.jdbcFetchSize = jdbcFetchSize;
        this.pageSize = pageSize;
    }

    /**
     * Construtor.
     * 
     * @param session session do hibernate.
     * @param criteria criteria.
     */
    public HibernateRandomAccessResultSet(Session session, Criteria criteria) {
        this(session, criteria, DEFAULT_FETCH_SIZE, DEFAULT_PAGE_SIZE);
    }

    /**
     * Construtor.
     * 
     * @param session session do hibernate.
     * @param query query.
     * @param jdbcFetchSize <code>Fetch Size</code> do <code>JDBC</code>.
     * @param pageSize tamanho da página da query.
     * @see Query#setFetchSize(int)
     */
    public HibernateRandomAccessResultSet(Session session, Query query, int jdbcFetchSize, int pageSize) {
        this.query = query;
        this.jdbcFetchSize = jdbcFetchSize;
        this.pageSize = pageSize;
    }

    /**
     * Construtor.
     * 
     * @param session session do hibernate.
     * @param query query.
     */
    public HibernateRandomAccessResultSet(Session session, Query query) {
        this(session, query, DEFAULT_FETCH_SIZE, DEFAULT_PAGE_SIZE);
    }

    /**
     * Devolve o elemento da posição indicada.
     * 
     * @param index posição.
     * @return o elemento da posição indicada.
     */
    public T getObject(int index) {
        if ((this.resultPage == null) || (index < this.pageStart) || (index > this.pageEnd)) {
            this.resultPage = this.getObjects(index, this.pageSize);
        }
        T result = null;
        int pos = index - this.pageStart;
        if ((this.resultPage != null) && (this.resultPage.size() > pos)) {
            result = this.resultPage.get(pos);
        }
        return result;
    }

    /**
     * Devolve a lista de objetos paginados.
     * 
     * @param firstResult o id do primeiro resultado.
     * @param maxResults número máximo de resultados.
     * @return a lista de objetos.
     */
    private List<T> getObjects(int firstResult, int maxResults) {
        List<T> queryResults;
        queryResults = this.executeQuery(firstResult, maxResults);
        if (this.resultPage == null) {
            this.resultPage = new ArrayList<T>(queryResults.size());
        }
        this.resultPage.clear();
        for (int i = 0; i < queryResults.size(); i++) {
            this.resultPage.add(queryResults.get(i));
        }
        this.pageStart = firstResult;
        this.pageEnd = firstResult + queryResults.size() - 1;

        try {
            this.loadLazy();
        } catch (Exception e) {
            throw new HibernateException(e);
        }

        return this.resultPage;
    }

    /**
     * Devolve a lista de objetos recuperados.
     * 
     * @param firstResult o id do primeiro resultado.
     * @param maxResults número máximo de resultados.
     * @return a lista de objetos.
     */
    @SuppressWarnings("unchecked")
    private List<T> executeQuery(int firstResult, int maxResults) {
        try {
            List<T> queryResults;
            if (this.criteria != null) {
                queryResults =
                        this.criteria.setCacheable(true).setFirstResult(firstResult).setMaxResults(maxResults)
                                .setFetchSize(this.jdbcFetchSize).list(); // no lugar do page size estava 100
            } else {
                queryResults =
                        this.query.setCacheable(true).setFirstResult(firstResult).setMaxResults(maxResults)
                                .setFetchSize(this.jdbcFetchSize).list(); // no lugar do page size estava 100
            }
            return queryResults;
        } catch (HibernateException e) {
            throw new PersistenceException(PersistMessages.ERROR_EXECUTING_QUERY.create(), e);
        }
    }

    /**
     * Metodo que carrega os atributos lazy
     * 
     * @throws NoSuchMethodException Nao encontrou o metodo
     * @throws InvocationTargetException Erro ao invocar o metodo
     * @throws IllegalAccessException Acesso ao metodo errado
     */
    private void loadLazy() throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        if (this.fieldLazy == null) {
            return;
        }

        for (Object o : this.resultPage) {
            for (int i = 0, m = this.fieldLazy.length; i < m; i++) {
                PropertyUtils.getProperty(o, this.fieldLazy[i]);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public Iterator<T> iterator() {
        return new Iterator<T>() {

            private int current = 0;

            public boolean hasNext() {
                return HibernateRandomAccessResultSet.this.getObject(this.current) != null;
            }

            public T next() {
                return HibernateRandomAccessResultSet.this.getObject(this.current++);
            }

            public void remove() {
                throw new UnsupportedOperationException("Remove not supported");
            }
        };
    }
}